#!/usr/bin/python3
# -*- coding: utf-8 -*-

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.


import os
import sys
import subprocess
import glob
from random import choice
import string
from optparse import OptionParser
import subprocess
import shutil

# squelch mysqldb spurious warnings
import warnings

warnings.simplefilter("ignore")
# ---- This snippet of code adds the sources path and the waf configured PYTHONDIR to the Python path ----
# ---- We do this so cloud_utils can be looked up in the following order:
# ---- 1) Sources directory
# ---- 2) waf configured PYTHONDIR
# ---- 3) System Python path
for pythonpath in (
    "@PYTHONDIR@",
    os.path.join(
        os.path.dirname(__file__), os.path.pardir, os.path.pardir, "python", "lib"
    ),
):
    if os.path.isdir(pythonpath):
        sys.path.insert(0, pythonpath)
# ---- End snippet of code ----


def runCmd(cmds):
    process = subprocess.Popen(
        " ".join(cmds), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
    )
    stdout, stderr = process.communicate()
    if process.returncode != 0:
        raise Exception(stderr)
    return stdout.decode("utf-8")


class DBDeployer(object):
    parser = None
    options = None
    args = None
    isDebug = False
    mgmtsecretkey = None
    dbsecretkey = None
    encryptiontype = None
    dbConfPath = r"@MSCONF@"
    dbDotProperties = {}
    dbDotPropertiesIndex = 0
    encryptionKeyFile = "@MSCONF@/key"
    encryptionJarPath = "@COMMONLIBDIR@/lib/cloudstack-utils.jar"
    success = False
    magicString = "This_is_a_magic_string_i_think_no_one_will_duplicate"

    def preRun(self):
        def backUpDbDotProperties():
            dbpPath = os.path.join(self.dbConfPath, "db.properties")
            copyPath = os.path.join(self.dbConfPath, "db.properties.origin")

            if os.path.isfile(dbpPath):
                shutil.copy2(dbpPath, copyPath)

        backUpDbDotProperties()

    def postRun(self):
        def cleanOrRecoverDbDotProperties():
            dbpPath = os.path.join(self.dbConfPath, "db.properties")
            copyPath = os.path.join(self.dbConfPath, "db.properties.origin")
            if os.path.isfile(copyPath):
                if not self.success:
                    shutil.copy2(copyPath, dbpPath)
                os.remove(copyPath)

        cleanOrRecoverDbDotProperties()

    def info(self, msg, result=None):
        output = ""
        if msg is not None:
            output = "%-80s" % msg

        if result is True:
            output += "[ \033[92m%-2s\033[0m ]\n" % "OK"
        elif result is False:
            output += "[ \033[91m%-6s\033[0m ]\n" % "FAILED"
        sys.stdout.write(output)
        sys.stdout.flush()

    def debug(self, msg):
        msg = "DEBUG:%s" % msg
        sys.stdout.write(msg)
        sys.stdout.flush()

    def putDbProperty(self, key, value):
        if key in self.dbDotProperties:
            (oldValue, index) = self.dbDotProperties[key]
            self.dbDotProperties[key] = (value, index)
        else:
            self.dbDotProperties[key] = (value, self.dbDotPropertiesIndex)
            self.dbDotPropertiesIndex += 1

    def getDbProperty(self, key):
        if key not in self.dbDotProperties:
            return None
        (value, index) = self.dbDotProperties[key]
        return value

    def errorAndExit(self, msg):
        self.postRun()
        err = (
            """\n\nWe apologize for below error:
***************************************************************
%s
***************************************************************
Please run:

    cloud-setup-encryption -h

for full help
"""
            % msg
        )
        sys.stderr.write(err)
        sys.stderr.flush()
        sys.exit(1)

    def prepareDBFiles(self):
        def prepareDBDotProperties():
            dbpPath = os.path.join(self.dbConfPath, "db.properties")
            dbproperties = file(dbpPath).read().splitlines()
            newdbp = []
            emptyLine = 0
            for line in dbproperties:
                passed = False
                line = line.strip()
                if line.startswith("#"):
                    key = line
                    value = ""
                    passed = True
                if line == "" or line == "\n":
                    key = self.magicString + str(emptyLine)
                    value = ""
                    emptyLine += 1
                    passed = True

                try:
                    if not passed:
                        (key, value) = line.split("=", 1)
                except Exception as e:
                    err = """Wrong format in %s (%s):
Besides comments beginning "#" and empty line, all key-value pairs must be in formula of
                    key=value
for example:
                    db.cloud.username = cloud
                    """ % (
                        dbpPath,
                        line,
                    )
                    self.errorAndExit(err)
                self.putDbProperty(key, value)
            self.info("Preparing %s" % dbpPath, True)

        prepareDBDotProperties()

    def finalize(self):
        def finalizeDbProperties():
            entries = []
            for key in list(self.dbDotProperties.keys()):
                (value, index) = self.dbDotProperties[key]
                if key.startswith("#"):
                    entries.insert(index, key)
                elif key.startswith(self.magicString):
                    entries.insert(index, "")
                else:
                    entries.insert(index, "%s=%s" % (key, value))
            file(os.path.join(self.dbConfPath, "db.properties"), "w").write(
                "\n".join(entries)
            )

        self.info("Finalizing setup ...", None)
        finalizeDbProperties()
        self.info(None, True)
        self.success = True  # At here, we have done successfully and nothing more after this flag is set

    def processEncryptionStuff(self):
        def encrypt(value):
            cmd = [
                "java",
                "-classpath",
                '"' + self.encryptionJarPath + '"',
                "com.cloud.utils.crypt.EncryptionCLI",
                "-i",
                '"' + value + '"',
                "-p",
                '"' + self.mgmtsecretkey + '"',
            ]
            return runCmd(cmd).strip("\n")

        def saveMgmtServerSecretKey():
            if self.encryptiontype == "file":
                file(self.encryptionKeyFile, "w").write(self.mgmtsecretkey)

        def formatEncryptResult(value):
            return "ENC(%s)" % value

        def encryptDBSecretKey():
            self.putDbProperty(
                "db.cloud.encrypt.secret",
                formatEncryptResult(encrypt(self.dbsecretkey)),
            )

        def encryptDBPassword():
            dbPassword = self.getDbProperty("db.cloud.password")
            if dbPassword == "":
                return  # Don't encrypt empty password
            if dbPassword == None:
                self.errorAndExit(
                    "Cannot find db.cloud.password in %s"
                    % os.path.join(self.dbConfPath, "db.properties")
                )
            self.putDbProperty(
                "db.cloud.password", formatEncryptResult(encrypt(dbPassword))
            )

            usagePassword = self.getDbProperty("db.usage.password")
            if usagePassword == "":
                return  # Don't encrypt empty password
            if usagePassword == None:
                self.errorAndExit(
                    "Cannot find db.usage.password in %s"
                    % os.path.join(self.dbConfPath, "db.properties")
                )
            self.putDbProperty(
                "db.usage.password", formatEncryptResult(encrypt(usagePassword))
            )

        self.info("Processing encryption ...", None)
        self.putDbProperty("db.cloud.encryption.type", self.encryptiontype)
        saveMgmtServerSecretKey()
        encryptDBSecretKey()
        encryptDBPassword()
        self.info(None, True)

    def parseOptions(self):
        def parseOtherOptions():
            self.encryptiontype = self.options.encryptiontype
            self.mgmtsecretkey = self.options.mgmtsecretkey
            self.dbsecretkey = self.options.dbsecretkey
            self.isDebug = self.options.debug

        def validateParameters():
            if self.encryptiontype != "file" and self.encryptiontype != "web":
                self.errorAndExit(
                    'Wrong encryption type %s, --encrypt-type can only be "file" or "web'
                    % self.encryptiontype
                )

        # ---------------------- option parsing and command line checks ------------------------
        usage = """%prog [-e ENCRYPTIONTYPE] [-m MGMTSECRETKEY] [-k DBSECRETKEY] [--debug]

    This command sets up the CloudStack Encryption.

    """
        self.parser = OptionParser(usage=usage)
        self.parser.add_option(
            "-v",
            "--debug",
            action="store_true",
            dest="debug",
            default=False,
            help="If enabled, print the commands it will run as they run",
        )
        self.parser.add_option(
            "-e",
            "--encrypt-type",
            action="store",
            type="string",
            dest="encryptiontype",
            default="file",
            help="Encryption method used for db password encryption. Valid values are file, web. Default is file.",
        )
        self.parser.add_option(
            "-m",
            "--managementserver-secretkey",
            action="store",
            type="string",
            dest="mgmtsecretkey",
            default="password",
            help="Secret key used to encrypt confidential parameters in db.properties. A string, default is password",
        )
        self.parser.add_option(
            "-k",
            "--database-secretkey",
            action="store",
            type="string",
            dest="dbsecretkey",
            default="password",
            help="Secret key used to encrypt sensitive database values. A string, default is password",
        )

        (self.options, self.args) = self.parser.parse_args()
        parseOtherOptions()
        validateParameters()

    def run(self):
        try:
            self.preRun()
            self.parseOptions()
            self.prepareDBFiles()
            self.processEncryptionStuff()
            self.finalize()
        finally:
            self.postRun()

        print("")
        print("CloudStack has successfully setup Encryption")
        print("")


if __name__ == "__main__":
    o = DBDeployer()
    o.run()
